Skip to content

feat(lint): translation reference integrity + option-key validation (#3583) - #3806

Merged
os-zhuang merged 3 commits into
mainfrom
claude/app-metadata-validation-midr20
Jul 28, 2026
Merged

feat(lint): translation reference integrity + option-key validation (#3583)#3806
os-zhuang merged 3 commits into
mainfrom
claude/app-metadata-validation-midr20

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#3583 方案的 Phase 3 / R6 —— 标题里 option-key validation 的那一半。同 PR 接入 validate / lint / compile

问题:i18n 门禁只有一个方向

os i18n check 问的是「元数据期望、但 bundle 里没有的键」。反方向从来没人问过 —— bundle 里有、但元数据不认的键 —— 尽管 spec 早就把答案命名好了:TranslationDiffStatus 'redundant'TranslationCoverageResult.redundantKeys 声明了却没有任何生产者

HotCRM 审计里这个方向发布了两类问题:

  • bundle 键指向对象早已不再声明的字段(改名之后翻译留在原地);
  • select 选项翻译按显示 label 或按值的变体拼写(direct-mail 之于 direct_mailplanned 之于 planning)。

两类都不会崩 —— 这正是麻烦所在。解析器查不到就回落到源串,于是整屏看着都翻译好了,只有那一个字段/一个选项静悄悄没翻。

validate-translation-references

遍历 stack.translations 的每个 bundle,对照它随行的 stack:

必须指向
objects.{object} 本 stack 的对象,或平台对象
objects.{object}.fields.{field} 该对象声明的字段
objects.{object}.fields.{field}.options.{key} 选项的存储 value
._views / ._actions / ._sections / ._actions.*.params 视图 name / 绑定到该对象的动作 / fieldGroups[].key 或具名 section / 参数 name
apps.{app} / .navigation.{id} 应用 name / 导航项 id
dashboards.{dash} / .widgets.{id} / .actions.{actionUrl} 仪表盘 name / widget id / 头部 actionUrl
globalActions.{action} 没有 objectName 的动作

全部为 warningtranslation-target-unknown / translation-option-key-unknown)。孤儿键是「惰性」而非「损坏」,严重级应当如实说出这一点 —— 这比 R1/R2 的死引用(error)弱一档。诊断里带上可选的已声明名字;当键其实是显示 label 时直接点名对应的 value;并加了一个命名空间段建议通道:tasktodo_task(编辑距离 5,纯 Levenshtein 给不出,而这恰恰是最该给建议的一类)。

跨包对象沿用 §4 阶梯且保留 S3:注册表里的平台对象整棵跳过(stack 侧看不见它的字段,就不该评判),带平台前缀但没人注册的名字只在对象键上报一次,绝不半查子树。

刻意不判messagesvalidationMessagessettingssettingsCommonmetadataForms —— 它们的键属于应用代码、插件和平台自己的元数据类型注册表,不存在可枚举的合法集合,写规则就是在猜。同理,对象优先的 AppTranslationBundleo.*)是 translation 元数据类型、不是 stack.translations,其键被跳过而不是报错。

顺带修掉的真实实例(在示例应用里)

零误报的前提是先把真发现修在生产者侧(Prime Directive #12):

  • app-crmaccount_id / contact_id / opportunity_id(字段其实叫 account / contact / opportunity)、lead 上的 first_name / last_name(对象只有 name)、两个谁都没声明的 _sections、以及应用键 crm(应用是 crm_app,连带 sales/admin 两个导航键也都不存在)。
  • app-todo:三个 locale 的 bundle 全部键在 task,而对象是 todo_task —— 92 个看起来已翻译的字符串根本不可达,i18n 覆盖率棘轮把这一点记了下来(212 → 120 未翻译,基线已更新)。
    它的 translation-completeness.test.ts 把同一个错键写死成字面量,于是对着一个解析器永远找不到的 bundle 常绿 —— 一个和 bundle 达成一致、而不是和元数据达成一致的完备性测试。现改为从 Task.name 读取对象键。

验证

  • 误报地板:三个真实应用修复后 零 finding
  • 并且确认不是空转:探针显示遍历实际访问了 35 个对象节点、274 个字段键、94 个选项键、5 个应用键(app-showcase 未作任何修改,本来就是干净的:22 个对象节点 / 156 字段键 / 22 选项键 → 0)。这是 feat(lint): page-component field bindings + non-dashboard chart bindings (#3583) #3684 那次的教训 —— 遍历若静默为空,零误报就是假通过。
  • 测试:lint 492(新增 20 条,fixture 以 HotCRM 实例为原型)、cli 673、app-crm 20、app-todo 78 全过。
  • 门禁:eslintcheck:nul-bytescheck:i18ncheck:i18n-coveragecheck:doc-authoring 干净;lint 包 build(含 DTS)通过。

文档

  • content/docs/protocol/kernel/i18n-standard.mdx 新增「Orphan Keys and Option Keys」小节:写明被检查的键面、警告级别、以及刻意不检查的命名空间及其原因(ADR-0049 的标注纪律)。
  • skills/objectstack-i18n/SKILL.md:选项键按 value 不按 label,并说明这条现在会被 lint 出来。
  • 方案文档 §3 R6 标记为已落地,附范围说明。

剩余(不在本 PR)

R7 validate-ai-references(第 6 类,knowledge.indexes 半边仍卡在决策 D1)、R8(Tier-B 候选,需先出验证说明)。因此本 PR 用 Refs 而非 Closes 关联 #3583

Refs #3583 · #3640(方案)· #3657(Phase 1)· #3684(Phase 2)· #3698(R5)· ADR-0072 / ADR-0078 / ADR-0085

🤖 Generated with Claude Code

https://claude.ai/code/session_015r31hzuSN19iK8ATRPcCpS


Generated by Claude Code

…3583)

The i18n gate only ever ran forward: `os i18n check` asks which keys the
metadata expects that no bundle carries. Nothing asked the reverse — which
keys a bundle carries that no metadata claims — though the spec already names
the answer (`TranslationDiffStatus 'redundant'`, `redundantKeys`), both
declared with no producer.

That direction ships two failure modes, both in the HotCRM audit: bundles keyed
to fields an object no longer declares, and option translations keyed by the
display label or a variant spelling of the stored value (`direct-mail` for
`direct_mail`, `planned` for `planning`). Neither breaks anything — the
resolver finds nothing and renders the source string, so the screen looks
translated while one field quietly is not.

`validateTranslationReferences` walks every bundle in `stack.translations`
against the stack it ships with — objects, fields, option values, views,
actions and their params, sections, apps and nav ids, dashboards, widgets,
header actions, global actions — wired into `validate`, `lint` and `compile`.
All findings are warnings: an orphan key is inert, not broken.

Cross-package objects keep the S3 rule: a registered platform object is
skipped wholly (its fields are invisible to a stack lint), an unregistered
platform-prefixed name is reported once on the object key, and no subtree is
half-checked. `messages`, `validationMessages`, `settings`, `settingsCommon`
and `metadataForms` are deliberately unjudged — their keys belong to code,
plugins and the platform registry, so there is no universe to resolve against.

Two example apps carried the exact bug class and are fixed at the producer:
app-crm translated `account_id`/`contact_id`/`opportunity_id` (fields are
`account`/`contact`/`opportunity`), `first_name`/`last_name` on a lead that
declares `name`, two sections nothing declares, and app `crm` (it is
`crm_app`); app-todo keyed every bundle to `task` while the object is
`todo_task` — 92 strings that looked translated were unreachable, which the
coverage ratchet now records. Its completeness test hard-coded the same wrong
key, so it stayed green against a bundle no resolver could find; it now reads
the key from `Task.name`.

Verified: lint 492 / cli 673 tests pass; the rule reports zero findings across
app-crm, app-showcase and app-todo after the fixes, having visited 274 field
keys, 94 option keys and 5 app keys over 35 object nodes — a floor with
substance, not an empty walk.

Refs #3583

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015r31hzuSN19iK8ATRPcCpS
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 6:58am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/lint.

19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @objectstack/lint)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…alidation-midr20

# Conflicts:
#	scripts/i18n-coverage-baseline.json
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 06:46
…st it (#3583)

Running the new rule over HotCRM (~18k lines of shipped metadata) reported
~40 `_views` keys as orphans. Every one of them was correct; the rule was
wrong, in two compounding ways:

1. A view record is a CONTAINER, not a view. The default list lives at `list`
   and the named tabs at `listViews.<key>` / `formViews.<key>`, each of which
   may also carry its own `name`. Collecting only the record's own `name`
   makes every named tab look undeclared.
2. The object binding lives INSIDE the container (`list.data.object`), not at
   the record root. The record-level lookup resolved to nothing on the
   canonical shape, so the whole record was skipped — which is why the
   universe was empty enough to report the app's entire view surface.

The examples missed this because none of their bundles carried `_views` keys
at all: the walk was exercised, but that one branch of the universe never
was. A corpus with the keys present is what caught it — §6.1's point about a
false-positive floor only meaning something when the walk is non-empty applies
per branch, not per rule.

`collectViewRecord` now registers each container under the object THAT
container binds (falling back to the record, then to the default list), and
accepts both the map key and the inner `name` — authors write either, and the
key is what the console renders the tab from. Form sections are collected in
the same pass, so `_sections` gets the same binding fix. Objects carrying
views directly (`objects[].views`, `objects[].listViews`) go through one code
path with the owner pinned.

Three regression tests pin the HotCRM shape.

Residual on HotCRM after the fix: 6 findings, all true —
`apps.crm_enterprise.navigation.group_products` and `.group_analytics` across
three locales, where the app declares group_sales / group_work /
group_marketing / group_service / group_insights and neither translated id
exists.

Refs #3583

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015r31hzuSN19iK8ATRPcCpS
@os-zhuang
os-zhuang merged commit 2343099 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/app-metadata-validation-midr20 branch July 28, 2026 07:05
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…DR-0109 draft (#3820) (#3871)

* feat(lint,spec): AI surface-affinity rule, ADR-0063 doc completion, ADR-0109 draft (#3820)

Positioning work ahead of R7 (issue #3820): the agent authoring surface is
closed per ADR-0063, so the buildable AI rule is the one that checks a
statically provable, runtime-fatal contradiction — skill ↔ agent surface
affinity (ADR-0064 §3) — not agent.skills resolution.

- lint: new `validate-ai-surface-affinity` rule + tests, appended to
  REFERENCE_INTEGRITY_RULES so `validate`/`lint`/`compile` all pick it up.
  Error severity: the runtime throws on this binding at chat time. Unresolved
  skill names are deliberately out of scope (kernel skills are
  runtime-registered; #3820 D0/D2). False-positive floor read per branch
  (#3806): clean on the HotCRM-shaped corpus, `examples/` proves nothing here.
- spec: stack.zod.ts AI-slot prose no longer describes the withdrawn
  ADR-0040 model — agents marked platform-internal (ADR-0063 §2), tools
  marked declaration-only pending #3820 D0, skills named as THE extension
  primitive. app.defaultAgent re-documented as a surface-binding knob (its
  example was the withdrawn tenant-agent pattern). SkillSchema documents that
  per-skill `permissions` deliberately does not exist (ADR-0049) — the field
  HotCRM authored and Zod silently stripped.
- docs: ADR-0109 (Proposed) — the #3820 D0 decision: third-party tools are
  bindings to actions/flows; platform tool names become a conformance-tested
  registry; unblocks the R7 skill.tools branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHjroNkLkajskKbJaidko4

* chore: changeset + regenerate app reference for defaultAgent doc (#3820)

Check Changeset and the generated-docs gate in TypeScript Type Check both
flagged the previous commit: add the missing changeset (@objectstack/lint
minor, @objectstack/spec patch) and the regenerated
content/docs/references/ui/app.mdx row for the re-documented
`app.defaultAgent`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHjroNkLkajskKbJaidko4

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants